/*
 * This file is part of aion-lightning <aion-lightning.com>.
 *
 *  aion-lightning is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-lightning is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-lightning.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionemu.gameserver.services;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.TreeSet;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import javolution.util.FastMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.commons.database.DatabaseFactory;
import com.aionemu.gameserver.dataholders.MotionData;
import com.aionemu.gameserver.model.Gender;
import com.aionemu.gameserver.model.Race;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.templates.item.WeaponType;
import com.aionemu.gameserver.skillengine.model.Motion;
import com.aionemu.gameserver.skillengine.model.MotionTime;
import com.aionemu.gameserver.skillengine.model.SkillTemplate;
import com.aionemu.gameserver.skillengine.model.Times;
import com.aionemu.gameserver.skillengine.model.WeaponTypeWrapper;
import com.aionemu.gameserver.utils.PacketSendUtility;



/**
 * @author kecimis
 */
public class MotionLoggingService {
	
	private static Logger log = LoggerFactory.getLogger(MotionLoggingService.class);

	private FastMap<String, MotionLog> motionsMap = new FastMap<String, MotionLog>().shared();

	private boolean advancedLog = false;
	
	private boolean started = false;
	
	public static final MotionLoggingService getInstance() {
		return SingletonHolder.instance;
	}

	public void start() {
		if (started)
			return;
		this.started = true;
		//load data from sql
		this.loadFromSql();
	}
	
	public void logTime(Player player, SkillTemplate sk, int clientTime, double distance) {
		int currentAttackSpeed = 0;
		if (!started)
			return;
		if (sk == null)
			return;
		if (player.getEquipment().getMainHandWeaponType() == null)
			return;
		Motion motion = sk.getMotion();
		if (motion == null)
			return;
	
		currentAttackSpeed = player.getGameStats().getAttackSpeed().getCurrent();
		
		int skillId = sk.getSkillId();
		WeaponType mainHandWeapon = player.getEquipment().getMainHandWeaponType();
		WeaponType offHandWeapon = player.getEquipment().getOffHandWeaponType();
		String motionName = motion.getName();
		//clientTime is send from client
		int baseTime = clientTime;//adjusted time

		
		if (motion.getInstantSkill()) {
		 	PacketSendUtility.sendMessage(player, "Skill: "+skillId+" is instant");
		 	return;
		}
		else if (clientTime == 0) {
			PacketSendUtility.sendMessage(player, "ClientTime is 0 for skill: "+skillId);
			return;
		}
		if (motion.getName() == null)
			return;
	
		long ammoTime = 0;
		if (sk.getAmmoSpeed() != 0)
			ammoTime = Math.round(distance / sk.getAmmoSpeed() * 1000);//checked with client
		
		//adjusting with ammospeed
		baseTime -= ammoTime;
		
		//adjust clientTime if play speed is not 100
		if (motion.getSpeed() != 100) {
			baseTime /= motion.getSpeed();
			baseTime *= 100;
		}	
		
		// logging
		if (advancedLog) {
			PacketSendUtility.sendMessage(player, "skillId: " + sk.getSkillId() + " motionName: " + motionName);
			PacketSendUtility.sendMessage(player,	"attackSpeed: " + currentAttackSpeed + " mainHand: " + mainHandWeapon.toString() + " isDual: " + (offHandWeapon != null));
			PacketSendUtility.sendMessage(player, "clientTime: " + clientTime + " baseTime: " + baseTime + " playSpeed: "+ motion.getSpeed());
			PacketSendUtility.sendMessage(player, "ammoTime: " + ammoTime + " ammoSpeed: " + sk.getAmmoSpeed()+ " distance: " + distance);
			PacketSendUtility.sendMessage(player, "-------------------");
		}
		else
			PacketSendUtility.sendMessage(player, "motionName: " + motionName + " clientTime: " + clientTime + " baseTime: "
				+ baseTime);
		
		Race race = player.getRace();
		Gender gender = player.getGender();
		
		//create WeaponTypeWrapper
		WeaponTypeWrapper weapon = new WeaponTypeWrapper(mainHandWeapon, offHandWeapon);
		//check if its present
		if (this.isPresent(motionName, weapon, skillId, currentAttackSpeed, race, gender)) {
			log.info("motionName: "+motionName+" weapon: "+(offHandWeapon != null ? "dual" : mainHandWeapon.toString())+" skillId: "+skillId+" currentAttackSpeed: "+currentAttackSpeed
				+"baseTime: "+baseTime+" storedTime: "+this.getTime(motionName, weapon, skillId, currentAttackSpeed, race, gender));
			PacketSendUtility.sendMessage(player, "Its already stored. storedTime: "+this.getTime(motionName, weapon, skillId, currentAttackSpeed, race, gender));
			return;
		}
			
		//addtime
		if (this.addTime(motionName, weapon, skillId, currentAttackSpeed , race, gender, baseTime))
			PacketSendUtility.sendMessage(player, "BaseTime: "+baseTime+" for motion: "+motionName+" was added.");
		else
			PacketSendUtility.sendMessage(player, "Couldnt add baseTime: "+baseTime+" for motion: "+motionName+"!");

	}
		
	public void createAnalyzeFiles() {
		/*
		TreeMap<String, WeaponTime> resultsBase = new TreeMap<String, WeaponTime>();
		TreeMap<String, WeaponTime> resultsCap = new TreeMap<String, WeaponTime>();
		TreeMap<String, WeaponTime> results1000 = new TreeMap<String, WeaponTime>();
		for (Entry<String, MotionLog> entry : motionsMap.entrySet()) {
			String fileName = "motions/" + entry.getKey() + ".xml";
			WeaponTime weaponTimeBase = new WeaponTime();
			WeaponTime weaponTimeCap = new WeaponTime();
			WeaponTime weaponTime1000 = new WeaponTime();
			try {
				FileWriter fstream = new FileWriter(fileName);
				BufferedWriter out = new BufferedWriter(fstream);

				out.append("<motion name=\"" + entry.getKey() + "\">\n");

				if (entry.getValue() != null) {
					// loop through weaponType
					for (Entry<WeaponTypeWrapper, List<SkillTime>> entry2 : entry.getValue().getMotionLog().entrySet()) {
						WeaponTypeWrapper weapon =  entry2.getKey();
						out.append("\t<motionlog "+ weapon.toString() + "\">\n");

						if (entry2.getValue() == null)
							continue;
						for (SkillTime st : entry2.getValue()) {
							out.append("\t\t<skilltime skillId=\"" + st.getSkillId() + "\" attackspeed=\""
								+ st.getAttackSpeed() + "\" clienttime=\"" + st.getClientTime() + "\" race=\"" + st.getRace() + "\" gender=\"" + st.getGender() + "\" baseTime=\"" + this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()) + "\" >\n");
							//fill weaponTime
							weaponTimeBase.add(weapon, this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()));
							weaponTimeCap.add(weapon, this.recalculate("cap", weapon, st.getAttackSpeed(), st.getClientTime()));
							weaponTime1000.add(weapon, this.recalculate("1000", weapon, st.getAttackSpeed(), st.getClientTime()));

						}
						out.append("\t<motionlog/>\n");
					}
					
					//fill results
					resultsBase.put(entry.getKey(), weaponTimeBase);
					resultsCap.put(entry.getKey(), weaponTimeCap);
					results1000.put(entry.getKey(), weaponTime1000);
				}
				out.append("<motion/>");
				out.close();
				fstream.close();
			}
			catch (IOException e) {
				log.error("Error: " + e.getMessage());
			}
		}
		
		// create results files
		String fileName = "motions/results.xml";
		try {
			FileWriter fstream = new FileWriter(fileName);
			BufferedWriter out = new BufferedWriter(fstream);
			out.append("<base attack speed>\n");
			for (Entry<String, WeaponTime> entry : resultsBase.entrySet()) {
				out.append("<motion name=\"" + entry.getKey() + "\">\n");
				TreeMap<WeaponTypeWrapper, Integer> map = entry.getValue().process();
				
				for(Entry<WeaponTypeWrapper, Integer> entry2 : map.entrySet()) {
					out.append("\t<weapontime " + entry2.getKey().toString() + "\" time=\"" +entry2.getValue()+ "\" >\n");
				}
				out.append("<motion/>\n");
				map.clear();
			}
			out.append("<base attack speed/>\n\n");
			
			out.append("<cap attack speed>\n");
			for (Entry<String, WeaponTime> entry : resultsCap.entrySet()) {
				out.append("<motion name=\"" + entry.getKey() + "\">\n");
				TreeMap<WeaponTypeWrapper, Integer> map = entry.getValue().process();
				
				for(Entry<WeaponTypeWrapper, Integer> entry2 : map.entrySet()) {
					out.append("\t<weapontime " + entry2.getKey().toString() + "\" time=\"" +entry2.getValue()+ "\" >\n");
				}
				out.append("<motion/>\n");
				map.clear();
			}
			out.append("<cap attack speed/>\n\n");
			
			out.append("<1000 attack speed>\n");
			for (Entry<String, WeaponTime> entry : results1000.entrySet()) {
				out.append("<motion name=\"" + entry.getKey() + "\">\n");
				TreeMap<WeaponTypeWrapper, Integer> map = entry.getValue().process();
				
				for(Entry<WeaponTypeWrapper, Integer> entry2 : map.entrySet()) {
					out.append("\t<weapontime " + entry2.getKey().toString() + "\" time=\"" +entry2.getValue()+ "\" >\n");
				}
				out.append("<motion/>\n");
				map.clear();
			}
			out.append("<1000 attack speed/>\n");
			
			out.close();
			fstream.close();
		}
		catch (IOException e) {
			log.error("Error: " + e.getMessage());
		}
	*/
	}
	
	
	public void createFinalFile() {
		MotionData motionData = new MotionData();
		List<MotionTime> motionTimes = motionData.getMotionTimes();
		
		// create results
		TreeMap<String, List<WeaponTime>> results = new TreeMap<String, List<WeaponTime>>();
		for (Entry<String, MotionLog> entry : motionsMap.entrySet()) {
			WeaponTime weaponTimeAm = new WeaponTime(Race.ASMODIANS, Gender.MALE);
			WeaponTime weaponTimeAf = new WeaponTime(Race.ASMODIANS, Gender.FEMALE);
			WeaponTime weaponTimeEm = new WeaponTime(Race.ELYOS, Gender.MALE);
			WeaponTime weaponTimeEf = new WeaponTime(Race.ELYOS, Gender.FEMALE);
			if (entry.getValue() == null)
				continue;
			// loop through weaponType
			for (Entry<WeaponTypeWrapper, List<SkillTime>> entry2 : entry.getValue().getMotionLog().entrySet()) {
				WeaponTypeWrapper weapon = entry2.getKey();

				if (entry2.getValue() == null)
					continue;
				for (SkillTime st : entry2.getValue()) {
					switch (st.getRace()) {
						case ASMODIANS:
							if (st.getGender() == Gender.MALE)
								weaponTimeAm.add(weapon, this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()));
							else
								weaponTimeAf.add(weapon, this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()));
							break;
						case ELYOS:
							if (st.getGender() == Gender.MALE)
								weaponTimeEm.add(weapon, this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()));
							else
								weaponTimeEf.add(weapon, this.recalculate("base", weapon, st.getAttackSpeed(), st.getClientTime()));
							break;
					}
				}
				List<WeaponTime> weaponTimes = new ArrayList<WeaponTime>(4);
				weaponTimes.add(weaponTimeAm);
				weaponTimes.add(weaponTimeAf);
				weaponTimes.add(weaponTimeEm);
				weaponTimes.add(weaponTimeEf);
				// fill results
				results.put(entry.getKey(), weaponTimes);
			}
		}
		
		for (Entry<String, List<WeaponTime>> entry : results.entrySet()) {
			Set<WeaponTypeWrapper> listofWeapons = new TreeSet<WeaponTypeWrapper>();
			//ordering BOOK_2H BOW DAGGER_1H MACE_1H ORB_2H POLEARM_2H STAFF_2H SWORD_1H SWORD_2H dualwield
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.BOOK_2H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.BOW, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.DAGGER_1H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.MACE_1H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.ORB_2H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.POLEARM_2H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.STAFF_2H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.SWORD_1H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.SWORD_2H, null));
			listofWeapons.add(new WeaponTypeWrapper(WeaponType.SWORD_1H, WeaponType.SWORD_1H));
		
			//create MotionTime
			MotionTime motion = new MotionTime();
			motion.setName(entry.getKey());
			
			for (WeaponTime wt : entry.getValue()) {
				//process values
				TreeMap<WeaponTypeWrapper, Integer> map = wt.process();
				
				StringBuilder sb = new StringBuilder();
				boolean first = true;
				// create time
				for(WeaponTypeWrapper weapon : listofWeapons) {
					if (first) {
						sb.append((map.containsKey(weapon) ? map.get(weapon) : "0"));
						first = false;
					} else
						sb.append(","+(map.containsKey(weapon) ? map.get(weapon) : "0"));
				}
				
				Times times = new Times();
				times.setTimes(sb.toString());
				switch (wt.getRace()) {
					case ASMODIANS:
						if (wt.getGender() == Gender.MALE)
							motion.setAm(times);
						else
							motion.setAf(times);
						break;
					case ELYOS:
						if (wt.getGender() == Gender.MALE)
							motion.setEm(times);
						else
							motion.setEf(times);
						break;
				}
			}
			
			motionTimes.add(motion);
		}

		//marshall the final xml file
		marshallFile(motionData, "data/static_data/skills/new_motion_times.xml");
	}
	
	public static void marshallFile(Object templates, String file) {
		try {
			JAXBContext jaxbContext = JAXBContext.newInstance(templates.getClass());
			Marshaller marshaller = jaxbContext.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,
					new Boolean(true));
			marshaller.marshal(templates, new FileOutputStream(file));
		} catch (JAXBException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	/**
	 * method used to recalculate time to base, cap or given attackspeed
	 * 
	 * @param method
	 * @param weapon
	 * @param attackSpeed
	 * @param time
	 * @return
	 */
	private int recalculate(String method, WeaponTypeWrapper weapon, int attackSpeed, int time) {
		int finalTime = 0;
			
		TreeMap<WeaponType, float[]> list = new TreeMap<WeaponType,float[]>();
		
		float dagger[] = {600f,1200f};
		list.put(WeaponType.DAGGER_1H, dagger);
		float mace[] = {750f,1500f};
		list.put(WeaponType.MACE_1H, mace);
		float sword1h[] = {700f,1400f};
		list.put(WeaponType.SWORD_1H, sword1h);
		float bookorb[] = {1100f,2200f};
		list.put(WeaponType.BOOK_2H, bookorb);
		list.put(WeaponType.ORB_2H, bookorb);
		float polearm[] = {1400f,2800f};
		list.put(WeaponType.POLEARM_2H, polearm);
		float staff[] = {1000f,2000f};
		list.put(WeaponType.STAFF_2H, staff);
		float swordbow[] = {1200f,2400f};
		list.put(WeaponType.SWORD_2H, swordbow);
		list.put(WeaponType.BOW, swordbow);
		
		float speed = 0;
		if (method.equalsIgnoreCase("base")) {
			speed = list.get(weapon.getMainHand())[1];
			if (weapon.getOffHand() != null)
				speed += (list.get(weapon.getOffHand())[1] * 0.25);
		} else if (method.equalsIgnoreCase("cap")) {
			speed = list.get(weapon.getMainHand())[0];
			if (weapon.getOffHand() != null)
				speed += (list.get(weapon.getOffHand())[0] * 0.25);
		} else {
			try {
				speed = Float.parseFloat(method);
			} catch (Exception e) {
				//log
			}
		}
		
		finalTime = Math.round((float)time / (float)attackSpeed * speed);
		
		return finalTime;
	}
	
	//save to sql
	public void saveToSql() {
		Connection con = null;

		final String INSERT_QUERY = "INSERT INTO skill_motions (motion_name, weapon_type, off_weapon_type, skill_id, attack_speed, race, gender, time) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE motion_name = ?";

		try {
			con = DatabaseFactory.getConnection();
			PreparedStatement stmt = con.prepareStatement(INSERT_QUERY);
			for (Entry<String, MotionLog> entry : motionsMap.entrySet()) {
				String motionName = entry.getKey();
				// set motion_name
				stmt.setString(1, motionName);
				stmt.setString(9, motionName);
				if (entry.getValue() == null)
					continue;
				// loop through weaponType
				for (Entry<WeaponTypeWrapper, List<SkillTime>> entry2 : entry.getValue().getMotionLog().entrySet()) {
					String weaponType = (entry2.getKey().getMainHand() != null?entry2.getKey().getMainHand().toString():"null");
					String offWeaponType = (entry2.getKey().getOffHand() != null?entry2.getKey().getOffHand().toString():"null");
					// set weapon_type
					stmt.setString(2, weaponType);
					stmt.setString(3, offWeaponType);

					if (entry2.getValue() == null)
						continue;
					//sort by skillId
					Collections.sort(entry2.getValue());
					for (SkillTime st : entry2.getValue()) {
						stmt.setInt(4, st.getSkillId());
						stmt.setInt(5, st.getAttackSpeed());
						stmt.setString(6, st.getRace().toString());
						stmt.setString(7, st.getGender().toString());
						stmt.setInt(8, st.getClientTime());
						stmt.execute();
					}
				}
			}
			stmt.close();
		}
		catch (SQLException e) {
			log.error("MotionLoggingService", e);
		}
		finally {
			DatabaseFactory.close(con);
		}
	}
	
	//load from sql
	public void loadFromSql() {
		Connection con = null;

		final String SELECT_QUERY = "SELECT * FROM skill_motions";
		try {
			con = DatabaseFactory.getConnection();
			PreparedStatement stmt = con.prepareStatement(SELECT_QUERY);
			
			ResultSet resultSet = stmt.executeQuery();

			while (resultSet.next()) {
				String motionName = resultSet.getString("motion_name");
				WeaponType mainHandWeapon = WeaponType.valueOf(resultSet.getString("weapon_type"));
				WeaponType offHandWeapon = (resultSet.getString("off_weapon_type").contains("null")?null:WeaponType.valueOf(resultSet.getString("off_weapon_type")));
				int skillId = resultSet.getInt("skill_id");
				int attackSpeed = resultSet.getInt("attack_speed");
				int time = resultSet.getInt("time");
				String sRace = resultSet.getString("race");
				String sGender = resultSet.getString("gender");
				WeaponTypeWrapper weapon = new WeaponTypeWrapper(mainHandWeapon, offHandWeapon);
				
				Race race = null;
				Gender gender = null;
				try {
					race = Race.valueOf(sRace);
					gender = Gender.valueOf(sGender);
				} catch (Exception e) {
					log.info("cant load gender or race for motion_name: "+motionName);
				} finally {					
					this.addTime(motionName, weapon, skillId, attackSpeed, race, gender, time);
				}
			}
			resultSet.close();
			stmt.close();
		}
		catch (SQLException e) {
			log.error("MotionLoggingService", e);
		}
		finally {
			DatabaseFactory.close(con);
		}
	}
	
	public void reloadFromSql() {
		this.clearMotions();
		this.loadFromSql();
	}
	
	private void clearMotions() {
		for (MotionLog mLog : this.motionsMap.values()) {
			mLog.getMotionLog().clear();
		}
		this.motionsMap.clear();
	}
	
	private boolean isPresent(String motionName, WeaponTypeWrapper weapon, int skillId, int currentAttackSpeed, Race race, Gender gender) {
		if (motionsMap.containsKey(motionName))
			return motionsMap.get(motionName).isPresent(weapon, skillId, currentAttackSpeed, race, gender);
		
		return false;
	}
	
	private int getTime(String motionName, WeaponTypeWrapper weapon, int skillId, int currentAttackSpeed, Race race, Gender gender) {
		if (motionsMap.containsKey(motionName))
			return motionsMap.get(motionName).getTime(weapon, skillId, currentAttackSpeed, race, gender);
		 
		 return 0;
	 }
	
	public boolean addTime(String motionName, WeaponTypeWrapper weapon, int skillId,
		int currentAttackSpeed, Race race, Gender gender, int clientTime) {
		if (!motionsMap.containsKey(motionName)) {
			MotionLog motionLog = new MotionLog();
			boolean result = motionLog.addSkillTime(weapon, new SkillTime(skillId, currentAttackSpeed, race, gender,
				clientTime));
			motionsMap.put(motionName, motionLog);
			return result;
		}
		else
			return motionsMap.get(motionName).addSkillTime(weapon,
				new SkillTime(skillId, currentAttackSpeed, race, gender, clientTime));
	}

	public void setAdvancedLog(boolean bol) {
		this.advancedLog = bol;
	}
	
	public boolean getAdvancedLog() {
		return this.advancedLog;
	}
	
	
	private MotionLoggingService() {
		log.info("MotionLoggingService started.");
	}


	@SuppressWarnings("synthetic-access")
	private static class SingletonHolder {
		protected static final MotionLoggingService instance = new MotionLoggingService();
	}


 private class MotionLog {
	 private FastMap<WeaponTypeWrapper, List<SkillTime>> motionsForWeapons = new FastMap<WeaponTypeWrapper, List<SkillTime>>();
	 
	 public FastMap<WeaponTypeWrapper, List<SkillTime>> getMotionLog() {
		 return this.motionsForWeapons;
	 }
	 public boolean addSkillTime(WeaponTypeWrapper weapon, SkillTime skillTime) {
		 if (motionsForWeapons.containsKey(weapon)) {
			 if (!motionsForWeapons.containsValue(skillTime)) {
				 motionsForWeapons.get(weapon).add(skillTime);
				 return true;
			 }
		 } else {
			 List<SkillTime> list = new ArrayList<SkillTime>();
			 list.add(skillTime);
			 motionsForWeapons.put(weapon, list);
			 return true;
		 }
		 
		 return false;
	 }
	 
	 public int getTime(WeaponTypeWrapper weapon, int skillId, int currentAttackSpeed, Race race, Gender gender) {
			 if (motionsForWeapons.containsKey(weapon)) {
				 for (SkillTime st : motionsForWeapons.get(weapon)) {
					 if(st.getSkillId() == skillId && st.getAttackSpeed() == currentAttackSpeed
						 && st.getRace() == race && st.getGender() == gender )
						 return st.getClientTime();
				 }
			 }
		 
		 return 0;
	 }
	 
	 public boolean isPresent(WeaponTypeWrapper weapon, int skillId, int currentAttackSpeed, Race race, Gender gender) {
			 if (motionsForWeapons.containsKey(weapon)) {
				 for (SkillTime st : motionsForWeapons.get(weapon)) {
					 if(st.getSkillId() == skillId && st.getAttackSpeed() == currentAttackSpeed
						  && st.getRace() == race && st.getGender() == gender)
						 return true;
				 }
			 }
		 
		 return false;
	 }
 }
 
 private class SkillTime implements Comparable<SkillTime> {
	 private int skillId;
	 private int attackSpeed;
	 private int clientTime;
	 private Race race;
	 private Gender gender;
 
		public SkillTime(int skillId, int attackSpeed, Race race, Gender gender, int clientTime) {
			this.skillId = skillId;
			this.attackSpeed = attackSpeed;
			this.clientTime = clientTime;
			this.race = race;
			this.gender = gender;
		}

		@Override
		public int compareTo(SkillTime o) {
			if (skillId < o.getSkillId())
				return -1;
			else if (skillId > o.getSkillId())
				return 1;
			else
				return 0;
		}

		
		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + attackSpeed;
			result = prime * result + clientTime;
			result = prime * result + ((gender == null) ? 0 : gender.hashCode());
			result = prime * result + ((race == null) ? 0 : race.hashCode());
			result = prime * result + skillId;
			return result;
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			SkillTime other = (SkillTime) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (attackSpeed != other.attackSpeed)
				return false;
			if (clientTime != other.clientTime)
				return false;
			if (gender != other.gender)
				return false;
			if (race != other.race)
				return false;
			if (skillId != other.skillId)
				return false;
			return true;
		}

		public int getSkillId() {
			return this.skillId;
		}

		public int getAttackSpeed() {
			return this.attackSpeed;
		}
		
		public int getClientTime() {
			return this.clientTime;
		}

		public Race getRace() {
			return race;
		}

		public Gender getGender() {
			return gender;
		}

		private MotionLoggingService getOuterType() {
			return MotionLoggingService.this;
		}
	}
 
 private class WeaponTime {
	 private TreeMap<WeaponTypeWrapper,List<Integer>> values = new TreeMap<WeaponTypeWrapper,List<Integer>>();
	 private Race race;
	 private Gender gender;
	 
	public WeaponTime(Race race, Gender gender) {
		this.race = race;
		this.gender = gender;
	}

	/**
	 * @return the race
	 */
	public Race getRace() {
		return race;
	}

	/**
	 * @return the gender
	 */
	public Gender getGender() {
		return gender;
	}

	public void add(WeaponTypeWrapper weapon, int value) {
		 if (values.containsKey(weapon))
			values.get(weapon).add(value);
		else {
			List<Integer> list = new ArrayList<Integer>();
			list.add(value);
			values.put(weapon, list);
		}
	 }
	 
		public TreeMap<WeaponTypeWrapper, Integer> process() {
			TreeMap<WeaponTypeWrapper, Integer> weaponMap = new TreeMap<WeaponTypeWrapper, Integer>();
			
			for (Entry<WeaponTypeWrapper, List<Integer>> entry2 : values.entrySet()) {
				//logic to calculate one value per weaponType
				//count the element with the most occurencies
				int finalValue = 0;
				int maxFrequency = 0;
				int value = 0;
				int total = 0;
				for (Integer i : entry2.getValue()) {
					total += i;
					if (calculateFrequency(entry2.getValue(), i) > maxFrequency) {
						maxFrequency = calculateFrequency(entry2.getValue(), i);
						value = i;
					}
				}
				log.info("maxFrequency: "+maxFrequency+" value: "+value+" size: "+entry2.getValue().size());
				//if frequency of given value is higher than 70% take it, otherwise do Arithmetic mean
				if (Math.round((float)entry2.getValue().size() * 0.7f) <= maxFrequency)
					finalValue = value;
				else
					finalValue = total / entry2.getValue().size();
			
				
				log.info("weaponTime.process() finalValue: "+finalValue);
				weaponMap.put(entry2.getKey(), finalValue);
			}

			return weaponMap;
		}
 }
 
 private int calculateFrequency(List<Integer> list, int value) {
	 int frequency = 0;
	 
	 //10% tolerance
	 int min = Math.round((float)value * 0.90f);
	 int max = Math.round((float)value * 1.1f);
	 for (Integer i : list) {
		 if (i == null) continue;
		 if (min <= value && max >= value)
			 frequency++;
	 }
	 
	 return frequency;
 }
}
